/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ArkFile, ts } from "arkanalyzer";
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { FileMatcher, MatcherCallback, MatcherTypes } from "../../matcher/Matchers";
import { Defects, IssueReport } from "../../model/Defects";
import { Rule } from "../../model/Rule";
import { BaseChecker, BaseMetaData } from "../BaseChecker";
import { RuleListUtil } from "../../utils/common/DefectsList";

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'NoExAssignCheck');

interface Violation {
    line: number;
    character: number;
    endCharacter: number;
    message: string;
    filePath?: string;
}

export class NoExAssignCheck implements BaseChecker {
    readonly CATCH_NAME = 'catch';
    readonly THROW_NAME = 'throw ';
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    public metaData: BaseMetaData = {
        severity: 2,
        ruleDocPath: 'docs/no-ex-assign-check.md',
        description: 'Disallow reassigning exceptions in `catch` clauses.',
    };

    private fileMatcher: FileMatcher = {
        matcherType: MatcherTypes.FILE,
    };

    public registerMatchers(): MatcherCallback[] {
        const fileMatcher: MatcherCallback = {
            matcher: this.fileMatcher,
            callback: this.check,
        };
        return [fileMatcher];
    }

    public check = (target: ArkFile) => {
        if (target instanceof ArkFile) {
            let code = target.getCode();
            const myInvalidPositions = this.checkAction(code);
            myInvalidPositions.forEach((violation) => {
                violation.filePath = target.getFilePath();
                this.addIssueReport(violation);
            });
        }
    };

    private checkAction(sourceCode: string): Violation[] {
        const sourceFile = ts.createSourceFile(
            "temp.ts",
            sourceCode,
            ts.ScriptTarget.Latest,
            true
        );
        const violations: Violation[] = [];

        function checkNode(node: ts.Node) {
            if (ts.isCatchClause(node)) {
                const catchVariable = node.variableDeclaration?.name;
                const catchVariableText = catchVariable ? catchVariable.getText() : null;
                const destructuredVariables = new Set<string>();
                // Handle destructuring in catch clause
                if (catchVariable && ts.isObjectBindingPattern(catchVariable)) {
                    for (const element of catchVariable.elements) {
                        if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
                            destructuredVariables.add(element.name.text);
                        }
                    }
                } else if (catchVariable && ts.isArrayBindingPattern(catchVariable)) {
                    for (const element of catchVariable.elements) {
                        if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
                            destructuredVariables.add(element.name.text);
                        }
                    }
                }
                node.block.statements.forEach((statement: ts.Statement) => {
                    checkStatement(statement, catchVariableText, destructuredVariables);
                });
            }
            ts.forEachChild(node, checkNode);
        }

        function checkStatement(statement: ts.Statement, catchVariableText: string | null, destructuredVariables: Set<string>) {
            if (ts.isExpressionStatement(statement)) {
                const expression = statement.expression;
                // Check if the expression is a ParenthesizedExpression
                let innerExpression: ts.Expression = expression;
                if (ts.isParenthesizedExpression(expression)) {
                    innerExpression = expression.expression; // Get the inner expression
                }
                // Check for binary expressions (e.g., ex = 0)
                if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
                    const left = expression.left;
                    if (ts.isIdentifier(left) && (left.text === catchVariableText || destructuredVariables.has(left.text))) {
                        const { line, character } = sourceFile.getLineAndCharacterOfPosition(left.getStart());
                        const endCharacter = left.getEnd();
                        const endPosition = sourceFile.getLineAndCharacterOfPosition(endCharacter);
                        violations.push({
                            message: `Do not assign to the exception parameter`,
                            line: line + 1,
                            character: character + 1,
                            endCharacter: endPosition.character + 1
                        });
                    }
                }

                // Check for array destructuring assignments (e.g., [ex] = [])
                if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
                    const left = expression.left;
                    if (ts.isArrayLiteralExpression(left)) {
                        left.elements.forEach((element: ts.Expression) => {
                            if (ts.isIdentifier(element) && (element.text === catchVariableText || destructuredVariables.has(element.text))) {
                                const { line, character } = sourceFile.getLineAndCharacterOfPosition(element.getStart());
                                const endCharacter = element.getEnd();
                                const endPosition = sourceFile.getLineAndCharacterOfPosition(endCharacter);
                                violations.push({
                                    message: `Do not assign to the exception parameter`,
                                    line: line + 1,
                                    character: character + 1,
                                    endCharacter: endPosition.character + 1
                                });
                            }
                        });
                    }
                }
                // Check for object destructuring assignments (e.g., ({ x: ex = 0 } = {}))
                // Check if the inner expression is a BinaryExpression
                if (ts.isBinaryExpression(innerExpression) && innerExpression.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
                    const left = innerExpression.left;
                    const right = innerExpression.right;
                    // Check if the left side is an ObjectLiteralExpression
                    if (ts.isObjectLiteralExpression(left)) {
                        left.properties.forEach((property: ts.ObjectLiteralElementLike) => {
                            if (ts.isPropertyAssignment(property)) {
                                const initializer = property.initializer;
                                if (ts.isBinaryExpression(initializer) && initializer.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
                                    const leftIdentifier = initializer.left;
                                    if (ts.isIdentifier(leftIdentifier) && (leftIdentifier.text === catchVariableText || destructuredVariables.has(leftIdentifier.text))) {
                                        const { line, character } = sourceFile.getLineAndCharacterOfPosition(leftIdentifier.getStart());
                                        const endCharacter = leftIdentifier.getEnd();
                                        const endPosition = sourceFile.getLineAndCharacterOfPosition(endCharacter);
                                        violations.push({
                                            message: `Do not assign to the exception parameter`,
                                            line: line + 1,
                                            character: character + 1,
                                            endCharacter: endPosition.character + 1
                                        });
                                    }
                                }
                            }
                        });
                    }
                }

            }
        }
        checkNode(sourceFile);
        return violations;
    }

    private addIssueReport(violation: Violation) {
        this.metaData.description = violation.message;
        const severity = this.rule.alert ?? this.metaData.severity;
        let defect = new Defects(violation.line, violation.character, violation.endCharacter, this.metaData.description, severity, this.rule.ruleId,
            violation.filePath as string, this.metaData.ruleDocPath, true, false, false);
        this.issues.push(new IssueReport(defect, undefined));
        RuleListUtil.push(defect);
    }
}